Skip to content

feat!: agent-forward CLI — TTY-aware output, structured errors, --fields, table formatting [REL-12752]#660

Open
nieblara wants to merge 10 commits intomainfrom
REL-12752-tty-1
Open

feat!: agent-forward CLI — TTY-aware output, structured errors, --fields, table formatting [REL-12752]#660
nieblara wants to merge 10 commits intomainfrom
REL-12752-tty-1

Conversation

@nieblara
Copy link
Copy Markdown
Contributor

@nieblara nieblara commented Mar 18, 2026

Consolidates four agent-forward workstreams into a single PR. Originally stacked as #660#661#662#663, now merged into one branch for a clean merge to main.

Breaking Change

Default output is now JSON when stdout is not a TTY. Scripts, CI, and agent environments that pipe ldcli output and relied on the plaintext default will now receive JSON.

Escape hatches (pick one):

ldcli flags list --project my-proj --output plaintext   # explicit flag
export LD_OUTPUT=plaintext                               # env var
export FORCE_TTY=1                                       # restore old default

Resolution order: --json > --output > LD_OUTPUT > config file > TTY-based default.


What's included

1. TTY-aware default output — REL-12752

When stdout is not a terminal, --output defaults to json instead of plaintext. Follows the convention used by gh, glab, and kubectl. FORCE_TTY / LD_FORCE_TTY env vars restore the plaintext default (modeled on NO_COLOR).

Key files: cmd/root.go, cmd/root_test.go, README.md

2. Actionable error messages — REL-12754

Errors now preserve the HTTP status code and include a Suggestion: line for common failures (401, 403, 404, 409, 429). Empty or non-JSON error bodies from the API are normalized into structured JSON with code, message, and statusCode.

Key files: internal/errors/suggestions.go, internal/resources/client.go, internal/output/plaintext_fns.go

3. --fields flag — REL-12755

Filters JSON output to specified top-level fields. A single flag response can be 50KB+; --fields key,name,kind returns only those fields. Handles both singular and collection responses, preserves totalCount and _links, never filters error responses.

Key files: cmd/cliflags/flags.go, internal/output/resource_output.go

4. Table-formatted plaintext output — REL-15756

Replaces * name (key) bullet output with aligned tables for list responses and key-value blocks for singular responses. Column definitions registered for flags, projects, environments, members, and segments. Unknown resources fall back to the existing bullet format.

Key files: internal/output/table.go, internal/output/resource_output.go


Testing

Each workstream includes its own test coverage:

  • TTY precedence and non-TTY default tests (cmd/root_test.go)
  • Error suggestion mapping and plaintext formatting tests (internal/errors/suggestions_test.go, internal/output/plaintext_fns_internal_test.go)
  • Field filtering for singular, collection, and error responses (internal/output/resource_output_test.go)
  • Table rendering and column registry tests (internal/output/table_test.go)

Related Jira issues:


Note

Medium Risk
Medium risk due to multiple user-facing breaking output changes (non-TTY defaulting to JSON, new plaintext formatting, and altered error JSON shape/casing) that can break scripts and tests relying on previous output.

Overview
Changes CLI output defaults and shapes for agent/CI friendliness. When stdout is not a TTY, --output now defaults to json (with FORCE_TTY/LD_FORCE_TTY to retain plaintext), and docs/tests are updated to lock in precedence (--json > --output > LD_OUTPUT > config > TTY default).

Adds response shaping and richer formatting. Introduces global --fields to filter top-level JSON fields, and updates command output plumbing to pass Fields/ResourceName so plaintext lists for flags, projects, environments, members, and segments render as aligned tables and singular resources as key/value blocks (fallbacks preserve legacy bullets for unknown resources).

Standardizes error responses. Resource client now normalizes HTTP errors into structured JSON including statusCode and optional suggestion (plus consistent http.StatusText casing for empty-body errors), and plaintext error rendering appends a Suggestion: line when present.

Reviewed by Cursor Bugbot for commit 5b7bfe3. Bugbot is set up for automated code reviews on this repo. Configure here.


Related Jira issue: REL-12752: TTY detection for auto-JSON output

@launchdarkly-upra launchdarkly-upra bot changed the title (feat) adding TTY detection for auto-JSON output [REL-12752] (feat) adding TTY detection for auto-JSON output Mar 18, 2026
@nieblara nieblara changed the title [REL-12752] (feat) adding TTY detection for auto-JSON output feat(REL-12752): adding TTY detection for auto-JSON output Mar 18, 2026
Copy link
Copy Markdown

@JackDanger JackDanger left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Love that this follows the gh / glab convention — great precedent. This is the PR I spent the most time thinking about because it's the one most likely to bite someone.

1. This is a breaking change — should it bump the major version?
ldcli uses semver via release-please (v1→v2 was an OpenAPI spec change). Today, if a user has a script like:

ldcli flags list --project my-proj | grep "my-flag"

…running in CI (no TTY), it gets plaintext and the grep works. After this PR, it silently gets JSON and the grep matches nothing. The script doesn't fail — it just returns no results. That's the worst kind of breakage: silent.

Research shows that existing LD consumers almost universally pass -o json explicitly (sandbox-data-generator, gonfalon dogfood-flags.sh, agent-prompts guide). So the blast radius is likely small. But "likely small" and "zero" are different things, and this is a public CLI distributed via Homebrew and npm. Worth discussing whether this warrants a v3 bump, or at minimum a deprecation warning period where non-TTY plaintext emits a stderr warning before flipping the default.

2. FORCE_TTY is checked via raw os.Getenv while isTerminal is injected
You went to the effort of making isTerminal a parameter for testability — nice. But FORCE_TTY is read directly from os.Getenv inside NewRootCommand, which means tests use os.Setenv/os.Unsetenv. These are process-global mutations that aren't safe with t.Parallel(). Consider adding FORCE_TTY to the injected function or to the envChecker interface from #659.

3. The LD_OUTPUT env var test implies a contract
The test LD_OUTPUT=plaintext overrides non-TTY default documents that LD_OUTPUT controls the output format. Is this intentional new behavior or pre-existing? If new, it should be in the README/docs. If pre-existing, good to have the test — but make sure this doesn't interact unexpectedly with --output flag and FORCE_TTY (three-way precedence: flag > env > TTY detection).

4. The isTerminal == nil → JSON default is a footgun
If any future caller of NewRootCommand forgets to pass isTerminal, they silently get JSON-only output. Consider making this required (no nil check) or at least panicking on nil in debug builds.

5. Consider a stderr notice during the transition
gh shipped this same change and it was smooth because people expect it. But gh had it from early on. ldcli has been plaintext-default for its entire life. A transitional approach: when non-TTY triggers auto-JSON, emit a one-line stderr notice like note: non-interactive session detected, defaulting to JSON output. Use --output plaintext to override. You can remove it after a release cycle.

Base automatically changed from REL-12753-ldcli-agent-flag to main March 19, 2026 20:20
@nieblara nieblara requested review from a team and cspath1 March 20, 2026 02:53
isTerminal func() bool,
getenv func(string) string,
) (*RootCmd, error) {
if isTerminal == nil {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When would this be nil?

* [feat] Agent friendly error handling

* feat(REL-12755): adding --fields flag (#662)

* [feat] adding --fields flag

* feat(REL-15756): updating agent friendly and improved rich text output (#663)

feat(REL-15756) updating agent friendly and improved rich text output
@nieblara nieblara changed the title feat(REL-12752): adding TTY detection for auto-JSON output feat!: agent-forward CLI — TTY-aware output, structured errors, --fields, table formatting Apr 14, 2026
@launchdarkly-upra launchdarkly-upra bot changed the title feat!: agent-forward CLI — TTY-aware output, structured errors, --fields, table formatting [REL-12752] feat!: agent-forward CLI — TTY-aware output, structured errors, --fields, table formatting Apr 14, 2026
@nieblara nieblara changed the title [REL-12752] feat!: agent-forward CLI — TTY-aware output, structured errors, --fields, table formatting feat!: agent-forward CLI — TTY-aware output, structured errors, --fields, table formatting [REL-12752] Apr 14, 2026
Three tests in root_test.go still asserted the old `name (key)` bullet
format, but the toggle-on command now passes ResourceName: "flags" which
triggers key-value output. Update to match the actual output shape.

Made-with: Cursor
Remove the standalone `fields []string` positional parameter from
CmdOutput and pass fields exclusively through CmdOutputOpts.Fields.
This eliminates the dual-path API and simplifies every call site.

Also emit a stderr warning when --fields is used with plaintext output,
since the flag is silently ignored in that mode.

Made-with: Cursor
…ELOG

Add entries for the error JSON shape change (new statusCode/suggestion
fields, message casing) and the plaintext table format change. Both are
breaking changes that should be called out for v3.0.

Made-with: Cursor
Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 5b7bfe3. Configure here.

403: "You don't have permission for this action. Check your access token's role " +
"and custom role policies in LaunchDarkly settings.",
404: "Resource not found. Verify the project key, flag key, or environment key. " +
"Use `ldcli projects list` or `ldcli flags list` to see available resources.",
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

404 suggestion hardcodes specific resource commands

Medium Severity

The 404 suggestion hardcodes ldcli projects list and ldcli flags list instead of deriving the resource from the active command's context. With ~375 auto-generated resource commands, a user running e.g. ldcli segments get who hits a 404 is told to run ldcli projects list or ldcli flags list, which is misleading. The SuggestionForStatus function signature only accepts statusCode and baseURI — it has no resource context to parameterize the suggestion. This is the exact "Bad" example from the rule.

Fix in Cursor Fix in Web

Triggered by learned rule: Suggestion registry entries must use command/resource context, not hardcoded commands

Reviewed by Cursor Bugbot for commit 5b7bfe3. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants